﻿Imports System.ComponentModel
Imports System.Text.RegularExpressions
Imports Co9320.NamaAlert.Model
Imports System.Net
Imports System.Web
Imports System.IO
Imports Ionic.Zlib

Public Class LiveSite
    Implements ISystemMessageSender

    Private Structure LevelAndMemberCount
        Dim Level As Integer
        Dim MemberCount As Integer
    End Structure

    Private CommunitiesTable As Dictionary(Of Integer, LevelAndMemberCount)


#Region "Events"

    Public Event StationRetrieved As EventHandler(Of StationEventArgs)
    Protected Overridable Sub OnStationRetrieved(ByVal e As StationEventArgs)
        RaiseEvent StationRetrieved(Me, e)
    End Sub

    Public Event SendSystemMessage(ByVal sender As Object, ByVal e As NamaAlert.Model.SystemMessageEventArgs) Implements NamaAlert.Model.ISystemMessageSender.SendSystemMessage
    Protected Overridable Sub OnSendSystemMessage(ByVal e As SystemMessageEventArgs)
        RaiseEvent SendSystemMessage(Me, e)
    End Sub

    Public Event DownloadCompleted As EventHandler(Of EventArgs)
    Protected Overridable Sub OnDownloadCompleted()
        RaiseEvent DownloadCompleted(Me, New EventArgs)
    End Sub

#End Region

#Region "Properties"

    Private _UserDataFolder As String
    Private ReadOnly Property UserDataFolder() As String
        Get
            Return _UserDataFolder
        End Get
    End Property

    Private _IsCommunitiesTableEnabled As Boolean = True
    ''' <summary>
    ''' コミュニティのレベルと参加メンバー数のキャッシュの有効化
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property IsCommunitiesTableEnabled() As Boolean
        Get
            Return _IsCommunitiesTableEnabled
        End Get
        Set(ByVal value As Boolean)
            _IsCommunitiesTableEnabled = value
        End Set
    End Property

#End Region

    Public Sub New(ByVal userDataFolder As String)
        _UserDataFolder = userDataFolder
        Dim file = System.IO.Path.Combine(Me.UserDataFolder, My.Resources.StationArchiveOfflineFileName)
        If System.IO.File.Exists(file) Then
            DecomposeArchive()
        End If
    End Sub

#Region "Station Retrieving"

    Private WithEvents StationWorker As BackgroundWorker

    ''' <summary>
    ''' 放送局の名前(コミュニティの場合はレベルと参加メンバー数含む)を非同期で取得
    ''' </summary>
    ''' <param name="id">Station Id</param>
    ''' <remarks>結果は StationRetrieved で通知</remarks>
    Public Sub GetStationAsync(ByVal id As String)
        StationWorker = New BackgroundWorker
        StationWorker.RunWorkerAsync(id)
    End Sub

    ''' <summary>
    ''' 放送局の名前取得(コミュニティの場合はレベルと参加メンバー数含む)
    ''' </summary>
    ''' <param name="id"></param>
    ''' <returns>取得項目のみ値が格納された Station オブジェクト</returns>
    ''' <remarks></remarks>
    Public Function GetStation(ByVal id As String) As Station

        Dim station = New Station With {.Id = id}
        If station.Kind <> StationKind.Channel AndAlso station.Kind <> StationKind.Community Then
            Return Nothing
        End If

        Dim body = GetContent(New Uri(String.Format(System.Globalization.CultureInfo.InvariantCulture, _
                                      If(station.Kind = StationKind.Channel, _
                                         My.Resources.ChannelUriFormatForRetrieving, _
                                         My.Resources.CommunityUriFormatForRetrieving), station.Id)))
        Dim match As Match

        ' Name
        match = Regex.Match(body, If(station.Kind = StationKind.Channel, My.Resources.ChannelNameRegEx, My.Resources.CommunityNameRegExOnThumbPage), RegexOptions.Singleline)
        If match.Success Then
            station.Name = HttpUtility.UrlDecode(match.Groups("name").Value)
        End If
        If station.Kind = StationKind.Channel Then
            Return station
        End If

        ' Level
        match = Regex.Match(body, My.Resources.CommunityLevelRegExOnThumbPage, RegexOptions.Singleline)
        If match.Success Then
            Dim level As Integer
            If Integer.TryParse(match.Groups("level").Value, level) Then
                station.Level = level
            End If
        End If

        ' MemberCount
        match = Regex.Match(body, My.Resources.CommunityMemberCountRegExOnThumbPage, RegexOptions.Singleline)
        If match.Success Then
            Dim count As Integer
            If Integer.TryParse(match.Groups("member_count").Value, count) Then
                station.MemberCount = count
            End If
        End If


        If IsCommunitiesTableEnabled Then
            If CommunitiesTable Is Nothing Then
                CommunitiesTable = New Dictionary(Of Integer, LevelAndMemberCount)
            End If

            Try
                Dim castId = CInt(id.Substring(2))
                Dim pair = New LevelAndMemberCount With {.Level = station.Level, .MemberCount = station.MemberCount}
                If CommunitiesTable.ContainsKey(castId) Then
                    CommunitiesTable(castId) = pair
                Else
                    CommunitiesTable.Add(castId, pair)
                End If
            Catch ex As Exception

            End Try
        End If

        Return station
    End Function

    Private Sub StationWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles StationWorker.DoWork

        Dim id = DirectCast(e.Argument, String).ToLower
        e.Result = GetStation(id)

    End Sub

    Private Sub StationWorker_Completed(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles StationWorker.RunWorkerCompleted
        If e.Cancelled OrElse e.Result Is Nothing Then
            Exit Sub
        End If

        OnStationRetrieved(New StationEventArgs(DirectCast(e.Result, Station)))
        StationWorker.Dispose()
    End Sub


#End Region

#Region "Archive download"

    Private WithEvents DownloadWorker As BackgroundWorker

    Public Sub DownloadArchiveAsync()
        DownloadWorker = New BackgroundWorker
        DownloadWorker.RunWorkerAsync()
    End Sub

    Private Sub DownloadWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles DownloadWorker.DoWork
        Const maxRetryCount As Integer = 5 'MEMO リトライ回数

        Dim file = System.IO.Path.Combine(Me.UserDataFolder, My.Resources.StationArchiveOfflineFileName)
        For i = 0 To maxRetryCount - 1
            Try
                My.Computer.Network.DownloadFile(String.Format(System.Globalization.CultureInfo.InvariantCulture, _
                                                               My.Resources.StationArchiveUriFormat, My.Resources.StationArchiveOnlineFileName), file, "", "", False, 100 * 1000, True)
                Exit For
            Catch ex As Exception
                OnSendSystemMessage(New SystemMessageEventArgs(SystemMessageKind.Warning, "ニコ生アラートサーバーから放送局データの取得または保存に失敗しました。詳細: " & ex.Message))
                If i < maxRetryCount - 1 Then
                    System.Threading.Thread.Sleep(New TimeSpan(0, 0, 30))
                    Continue For
                End If
            End Try
        Next

        If Not System.IO.File.Exists(file) Then
            Exit Sub
        End If

        DecomposeArchive()
        e.Result = True
    End Sub

    Private Sub DownloadWorker_Completed(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles DownloadWorker.RunWorkerCompleted
        If e.Cancelled OrElse e.Result Is Nothing Then
            Exit Sub
        End If
        OnDownloadCompleted()
        DownloadWorker.Dispose()
    End Sub

    Private Sub DecomposeArchive()

        Dim file = System.IO.Path.Combine(Me.UserDataFolder, My.Resources.StationArchiveOfflineFileName)
        If CommunitiesTable Is Nothing Then
            CommunitiesTable = New Dictionary(Of Integer, LevelAndMemberCount)
        End If

        Try
            Using fs = New FileStream(file, FileMode.Open, FileAccess.Read), _
                  gzip = New ZlibStream(fs, Ionic.Zlib.CompressionMode.Decompress), _
                  ms = New MemoryStream, _
                  br = New BinaryReader(ms)

                Dim buffer(1023) As Byte
                Do
                    Dim length = gzip.Read(buffer, 0, buffer.Length)
                    If length <= 0 Then
                        Exit Do
                    End If
                    ms.Write(buffer, 0, length)
                Loop

                ms.Position = 0
                For i = 0 To ms.Length \ 8 - 1
                    Dim id = br.ReadInt32
                    Dim value = br.ReadInt32()
                    Dim pair = New LevelAndMemberCount
                    pair.Level = value >> 24
                    pair.MemberCount = value And &HFFFFFF

                    If CommunitiesTable.ContainsKey(id) Then
                        CommunitiesTable(id) = pair
                    Else
                        CommunitiesTable.Add(id, pair)
                    End If
                Next
            End Using

        Catch ex As Exception

        End Try
    End Sub

    ''' <summary>
    ''' コミュニティのレベルと参加メンバー数を格納したStationを返す（ない場合はNothingを返す）
    ''' </summary>
    ''' <param name="id"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetCommunityLevelAndMemberCount(ByVal id As String) As Station

        If CommunitiesTable Is Nothing Then
            Return Nothing
        End If

        Dim station = New Station With {.Id = id}
        If station.Kind <> StationKind.Community Then
            Return Nothing
        End If

        Try
            Dim castId = CInt(id.Substring(2))
            If CommunitiesTable.ContainsKey(castId) Then
                station.Level = CommunitiesTable(castId).Level
                station.MemberCount = CommunitiesTable(castId).MemberCount
                Return station
            End If
        Catch ex As Exception

        End Try
        Return Nothing
    End Function

#End Region


    Private Shared Function GetContent(ByVal uri As Uri) As String

        Dim request = DirectCast(WebRequest.Create(uri), HttpWebRequest)
        request.AutomaticDecompression = DecompressionMethods.GZip Or DecompressionMethods.Deflate

        'request.UserAgent = My.Settings.UserAgent
        'If My.Settings.ProxyUserName <> "" OrElse _
        '   My.Settings.ProxyPassword <> "" Then
        '    request.Credentials = New NetworkCredential(My.Settings.ProxyUserName, My.Settings.ProxyPassword)
        'End If

        Dim content As String = ""
        Try
            Dim response = request.GetResponse
            Using stream = response.GetResponseStream, _
                  reader = New System.IO.StreamReader(stream, System.Text.Encoding.UTF8)
                content = reader.ReadToEnd
            End Using
        Catch webEx As WebException
            ' 
        End Try
        Return content

    End Function

End Class
